Technote 1119Serial Port ApocryphaApple Developer Technical Support |
CONTENTSNotes for
Both APIs |
This Technote describes a number of problems often encountered by developers when dealing with serial ports under Mac OS. Most of this information is available from other sources, but those sources are obscure and commonly overlooked. Specifically, this Note describes the correct techniques for finding, opening, closing, and yielding serial ports under the classic serial API and the Open Transport serial API. In addition, this Note describes the theory and practice of the original and Open Transport serial port arbitrators. This Technote is directed at all Mac OS developers who use serial ports. |
Notes for Both APIsMac OS provides two APIs for accessing the serial port:
the classic serial API based on Device Manager
Open and Close on DemandA serial port is a non-sharable resource. If your application has the port open, no other application can open it. For this reason, you should always open and close the serial port on demand. For example, if your application only uses the serial port as part of its registration process, you open the port when you commence the registration and close the port immediately after you are done. YieldingYielding is the process by which a passive serial program can yield the serial port to an active serial program, and regain the serial port after the active serial program is done. For example, if you set Apple Remote Access (version 2.1 and lower) to wait for an incoming call, you can still make outgoing PPP connections using FreePPP. This is because the passive serial program (ARA) yields the serial port to the active serial program (FreePPP). When FreePPP closes the serial port, ARA will resume ownership and continue waiting for an incoming call. |
Just The Facts: Classic SerialThe classic serial architecture is based on Device Manager
Finding All Serial PortsThe correct way to find all the serial ports under Mac OS is to
use the Communications Resource Manager (CRM) routine
|
static void PrintInfoAboutAllSerialPorts(void) // Prints a list of all the serial ports on the // machine, along with their corresponding input // and output driver names, to stdout. Typically // you would use a routine like this to populate a // popup menu of the available serial ports. { CRMRec commRecord; CRMRecPtr thisCommRecord; CRMSerialPtr serialPtr; (void) InitCRM(); // First set up commRecord to specify that // we're interested in serial devices. commRecord.crmDeviceType = crmSerialDevice; commRecord.crmDeviceID = 0; // Now repeatedly call CRMSearch to iterate // through all the serial ports. thisCommRecord = &commRecord; do { thisCommRecord = (CRMRecPtr) CRMSearch( (CRMRecPtr) thisCommRecord ); if ( thisCommRecord != nil ) { // Once we a have a CRMRec for the serial port, // we must cast the crmAttributes field to // a CRMSerialPtr to access to serial-specific // fields about the port. serialPtr = (CRMSerialPtr) thisCommRecord->crmAttributes; // Print the information about the port. printf("We have a port called: '%#s'\n", *(serialPtr->name)); printf(" input driver named: '%#s'\n", *(serialPtr->inputDriverName)); printf(" output driver named: '%#s'\n", *(serialPtr->outputDriverName)); printf("\n"); // Now ensure that CRMSearch finds the next device. commRecord.crmDeviceID = thisCommRecord->crmDeviceID; } } while ( thisCommRecord != nil ); } |
NOTE: |
IMPORTANT: |
The correct way to open a serial port has been documented for many years as part of the ARA API document, currently available on the Mac OS SDK Developer CDs. However, this source is somewhat obscure (and the enclosed sample code is somewhat out of date), so the information is repeated here for your convenience. The process is very easy to describe in English:
This high-level algorithm is captured in the following routines for opening both the input and output serial drivers: |
static OSErr OpenOneSerialDriver(ConstStr255Param driverName, short *refNum) // The one true way of opening a serial driver. This routine // tests whether a serial port arbitrator exists. If it does, // it relies on the SPA to do the right thing when OpenDriver is called. // If not, it uses the old mechanism, which is to walk the unit table // to see whether the driver is already in use by another program. { OSErr err; if ( SerialArbitrationExists() ) { err = OpenDriver(driverName, refNum); } else { if ( DriverIsOpen(driverName) ) { err = portInUse; } else { err = OpenDriver(driverName, refNum); } } return err; } static OSErr OpenSerialDrivers(ConstStr255Param inName, ConstStr255Param outName, SInt16 *inRefNum, SInt16 *outRefNum) // Opens both the input and output serial drivers, and returns their // refNums. Both refNums come back as an illegal value (0) if we // can't open either of the drivers. { OSErr err; err = OpenOneSerialDriver(outName, outRefNum); if (err == noErr) { err = OpenOneSerialDriver(inName, inRefNum); if (err != noErr) { (void) CloseDriver(*outRefNum); } } if (err != noErr) { *inRefNum = 0; *outRefNum = 0; } return err; } |
The above code opens the output serial driver before opening the
input serial driver. This is the recommended order for the built-in
serial drivers, and consequently for other CRM-registered serial
drivers. This is because the output driver is the one that reserves
system resources and actually checks for the availability of the
port. For the built-in serial ports, if you successfully open the
output driver, you should always be able to open the input driver.
Not all CRM-registered serial drivers work this way, however, so your
code should always check the error result from both opens.
The code for determining whether a serial port arbitrator is installed is shown below: |
enum { gestaltSerialPortArbitratorAttr = 'arb ', gestaltSerialPortArbitratorExists = 0 }; static Boolean SerialArbitrationExists(void) // Test Gestalt to see if serial arbitration exists // on this machine. { Boolean result; long response; result = ( Gestalt(gestaltSerialPortArbitratorAttr, &response) == noErr && (response & (1 << gestaltSerialPortArbitratorExists) != 0) != 0) ); return result; } |
The final part of the puzzle is the routine
DriverIsOpen , which walks the unit table to see if the
driver serial driver is present and open. Remember that this routine
-- which is inherently evil because it accesses low memory globals --
is only used if a serial port arbitrator is not installed.
|
static Boolean DriverIsOpen(ConstStr255Param driverName) // Walks the unit table to determine whether the // given driver is marked as open in the table. // Returns false if the driver is closed, or does // not exist. { Boolean found; Boolean isOpen; short unit; DCtlHandle dceHandle; StringPtr namePtr; found = false; isOpen = false; unit = 0; while ( ! found && ( unit < LMGetUnitTableEntryCount() ) ) { // Get handle to a device control entry. GetDCtlEntry // takes a driver refNum, but we can convert between // a unit number and a driver refNum using bitwise not. dceHandle = GetDCtlEntry( ~unit ); if ( dceHandle != nil && (**dceHandle).dCtlDriver != nil ) { // If the driver is RAM based, dCtlDriver is a handle, // otherwise it's a pointer. We have to do some fancy // casting to handle each case. This would be so much // easier to read in Pascal )-: if ( ((**dceHandle).dCtlFlags & dRAMBasedMask) != 0 ) { namePtr = & (**((DRVRHeaderHandle) (**dceHandle).dCtlDriver)).drvrName[0]; } else { namePtr = & (*((DRVRHeaderPtr) (**dceHandle).dCtlDriver)).drvrName[0]; } // Now that we have a pointer to the driver name, compare // it to the name we're looking for. If we find it, // then we can test the flags to see whether it's open or // not. if ( EqualString(driverName, namePtr, false, true) ) { found = true; isOpen = ((**dceHandle).dCtlFlags & dOpenedMask) != 0; } } unit += 1; } return isOpen; } |
NOTE: |
If you successfully open a serial port, you should make sure to
close it again when you're done. You should always use
It's important that you close the serial driver, even if your application quits abnormally. If you fail to close the serial driver when you quit, it will be unavailable for other applications until the computer is restarted. The following techniques are ways to ensure that you close the serial driver even if your application quits abnormally:
|
NOTE: |
The classic serial architecture has very limited support for yielding the serial port. Apple Remote Access does this using a private API exported by the Link Tool Manager (part of ARA). This API was never published by Apple, and is not available to third parties. If your application requires serial port yielding, you might want to investigate using the OT serial API. Just The Facts: Open Transport SerialOpen Transport provides a second API for serial on Mac OS, one that has much in common with the network APIs provided by OT. In the current implementation of OT (version 1.3 at the time of writing), the OT serial API is implemented as a shim layered on top of the classic serial drivers. This fact is important because the way you use the OT serial API affects the availability of serial ports to the classic API, and vice versa. Inside Macintosh: Open Transport contains a lot of background material that you might find useful. Finding All Serial PortsIf you are using the OT serial API, the correct way to find all
the installed serial ports is to repeatedly call
Yielding
|
A Tale of Two ArbitratorsThe Serial Port Arbitrator is one of the least understood components of the Mac OS, partly because it is installed by Apple Remote Access and is not a core component of the system. This section explains why serial port arbitration is necessary, and the features of the two serial port arbitrators. The Original ProblemThe original Mac OS Device Manager architecture has an
interesting 'quirk' in that, once a driver is opened, any
further calls to On pre-MultiFinder Macintoshes, this was never a problem because only one program could be running at a time, and presumably it had control of the serial ports. However, with the advent of MultiFinder, multiple applications could be running simultaneously, and so the serial port ownership became an issue. The Original SolutionThe original solution was fairly easy: if the serial port is already open, it must be in use by another application, and hence you should not try to use it. While this requires serial applications to poke around in the unit table, it was a perfectly serviceable solution. The New ProblemThe new problem arose with the advent of Apple Remote Access. ARA has a mode in which it will passively sit in the background waiting for calls. However, users were annoyed by the fact that ARA was permanently using their serial port (and, more specifically, their modem), so they could not make outgoing calls without first turning off ARA's answer mode. This problem was hard to get around because of the
original solution. A well-behaved application looked in the
unit table, noticed that the serial driver was in use, and
did not even attempt to call The New SolutionThe solution to this new problem was twofold. First, the
rules were changed for developers. The new rule is the one
described above: if a serial port
arbitrator is installed, applications should ignore the unit
table and always call Second, ARA shipped with the Serial Port Arbitrator . The
Serial Port Arbitrator patches
The original Serial Port Arbitrator shipped as part of ARA 1.0. Its operation was intimately tied with the ARA Link Tool Manager. The Link Tool Manager API, which ARA uses to open a serial port in passive mode, was never publically documented. The Newer SolutionUnfortunately, in computers, stability is death, and this
is as true for ARA as it is for any other part of Mac OS.
Part of the plan for ARA 3.0 was to get rid of the Link Tool
Manager, and its associated Serial Port Arbitrator. However,
by the time ARA 3.0 became a reality, developers were used
to the Serial Port Arbitrator and were happily calling
So ARA 3.0 includes a new serial port arbitrator, the
OpenTpt Serial Arbitrator, which includes the serial port
arbitration functionality of the original Serial Port
Arbitrator. Like the original Serial Port Arbitrator, the
OpenTpt Serial Arbitrator patches
The Latest ProblemsAlas, Mac OS has still to achieve serial port arbitration nirvana. A number of serious deficiencies remain in the OpenTpt Serial Arbitrator:
This note will be revised as these problems are addressed. |
SummaryThe Mac OS serial port is a shared resource, and the true owner of this resource -- the user -- gets upset when their serial programs do not play well together. By following the guidelines outlined in this note, your program will correctly find all the serial ports on the machine, use those serial ports in the most co-operative way, and be adored by Macintosh users around the world! |
|
Thanks to Brian Bechtel, Peter Gontier, Bo3b Johnson, Matt Mora, Roger Pantos, Craig Prouse, and George Warner.